﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections.ObjectModel;
using System.Xml;
using System.Net;
using System.IO;
using System.Configuration;
using System.Xml.Linq;
using System.Threading;
using System.ComponentModel;
using System.Windows;
using System.Windows.Input;
using System.Windows.Threading;
using MySql.Data.MySqlClient;
using LicenseData;

namespace NetLogger
{
	public enum NetServerStatuses
	{
		ServerOk,
		ServerUpdate,
		ServerWarning,
		ServerError
	}

	/// <summary>
	/// Defines the main class for a net activation (only one of these will be instantiated during program execution)
	/// </summary>
	public class NetActivation : NetPropertyBase, ICommand
	{
		public const string WebRequestContentType = "application/x-www-form-urlencoded";

		/// <summary>
		/// Web page for retrieving list of nets, starting a new net, and updating an active net
		/// </summary>
		public const string NetsPage = "nets.php";
		/// <summary>
		/// Web page for posting activity updates for an active net
		/// </summary>
		public const string ActivityPage = "activity.php";
		/// <summary>
		/// Web page for downloading updates from other loggers during an active net
		/// </summary>
		public const string SyncPage = "sync.php";
		/// <summary>
		/// Web page for posting roster additions and updates for an active net
		/// </summary>
		public const string RosterPage = "roster.php";
		/// <summary>
		/// Web page for posting messages for an active net
		/// </summary>
		public const string MessagePage = "messages.php";
		/// <summary>
		/// Web page for downloading updated FCC license data
		/// </summary>
		public const string LicenseUpdatesPage = "license_updates.php";

		/// <summary>
		/// Wait time (ms) after a post error before retrying
		/// </summary>
		public const int PostErrorWait = 3000;
		/// <summary>
		/// Time (ms) between queries to APRS database
		/// </summary>
		public const int AprsQueryInterval = 30000;
		/// <summary>
		/// Revision number to match against PHP pages to ensure client/web compatibility
		/// </summary>
		public const int WebRevision = 2;

		/// <summary>
		/// MySQL DB connection string for APRS DB (username/password obtained from aprsworld.net)
		/// </summary>
		private const string AprsConnectionString = "server=db.aprsworld.net;uid=kd4alu;pwd=rex;database=aprs";

		/// <summary>
		/// String to append to addresses that identifies the geographic region to search within for the address
		/// (e.g. a city or county)
		/// </summary>
		public static readonly string GeocodeRegionSuffix = ConfigurationManager.AppSettings[ "GeocodeRegionSuffix" ];
		/// <summary>
		/// Time (seconds) between polling the web site for updates from other loggers
		/// </summary>
		public static readonly int UpdateInterval = int.Parse( ConfigurationManager.AppSettings[ "SyncInterval" ] );
		/// <summary>
		/// Array of keywords that identify a report as priority when found in the body of the report
		/// </summary>
		public static readonly string[] PriorityKeywords = ConfigurationManager.AppSettings[ "PriorityKeywords" ].Split( ',' );
		/// <summary>
		/// Regular expression for a valid amateur radio callsign
		/// </summary>
		public static readonly string CallsignRegEx = ConfigurationManager.AppSettings[ "CallsignRegEx" ];
		/// <summary>
		/// URL pointing to the location where the client PHP files reside
		/// </summary>
		public static readonly string WebUrl = ConfigurationManager.AppSettings[ "WebURL" ];
		/// <summary>
		/// Minimum latitude to accept when geocoding addresses
		/// </summary>
		public static readonly double MinLatitude;
		/// <summary>
		/// Minimum longitude to accept when geocoding addresses
		/// </summary>
		public static readonly double MinLongitude;
		/// <summary>
		/// Maximum latitude to accept when geocoding addresses
		/// </summary>
		public static readonly double MaxLatitude;
		/// <summary>
		/// Maximum longitude to accept when geocoding addresses
		/// </summary>
		public static readonly double MaxLongitude;

		private LinkedList<NetPost> _netPosts = new LinkedList<NetPost>();
		private EventWaitHandle _newPost;
		private EventWaitHandle _terminate;
		private Thread _processPosts;
		private Thread _sync;
		private Thread _queryAprs;
		private MainWindow _window;
		private DateTimeOffset _recordTime;
		private string _callsignSearch;
		private DispatcherTimer _uiTimer;
		private NetMapProxy _mapProxy;
		private NetErrorLog _errorLog;
		private NetMember _scrollToMember;
		private Dictionary<string, NetAprs> _aprsData = new Dictionary<string, NetAprs>();
		private string _netLoggerId;
		private LicenseDictionary _licenseData;

		private static NetActivation _this;
		private static bool _aprsMatchesUpdated;

		private delegate void SyncUpdateDelegate( XDocument doc );
		private delegate void UpdateAprsAllDelegate();

		public delegate void ExitingFeedbackDelegate();

		/// <summary>
		/// Initializes static members
		/// </summary>
		static NetActivation()
		{
			string[]
				locationRange;

			locationRange = ConfigurationManager.AppSettings[ "LocationRange" ].Split( ',' );
			MinLatitude = Convert.ToDouble( locationRange[ 0 ] );
			MinLongitude = Convert.ToDouble( locationRange[ 1 ] );
			MaxLatitude = Convert.ToDouble( locationRange[ 2 ] );
			MaxLongitude = Convert.ToDouble( locationRange[ 3 ] );
			_aprsMatchesUpdated = false;
		}

		/// <summary>
		/// Creates a new net activation tied to the given main window
		/// </summary>
		/// <param name="window">Main UI window for the activation</param>
		public NetActivation( MainWindow window )
		{
			_this = this;
			_window = window;
			_frequencies = ConfigurationManager.AppSettings[ "Frequencies" ].Split( ',' );
			_selectedFrequency = _frequencies[ 0 ];
			_newPost = new EventWaitHandle( false, EventResetMode.AutoReset );
			_terminate = new EventWaitHandle( false, EventResetMode.ManualReset );
			_processPosts = new Thread( new ThreadStart( ProcessPosts ) );
			_sync = new Thread( new ThreadStart( Sync ) );
			_queryAprs = new Thread( new ThreadStart( QueryAprs ) );
			_errorLog = new NetErrorLog( _terminate );
			_licenseData = LicenseDictionary.GetLicenseDictionary();
		}

		/// <summary>
		/// Singleton instance of the net activation object
		/// </summary>
		public static NetActivation Instance
		{
			get
			{
				return _this;
			}
		}

		private ObservableCollection<NetMember> _members = new ObservableCollection<NetMember>();
		/// <summary>
		/// Roster of all stations checked into the net (including inactive stations)
		/// </summary>
		public ObservableCollection<NetMember> Members
		{
			get
			{
				return _members;
			}
		}

		private ObservableCollection<NetMember> _nearestStations = new ObservableCollection<NetMember>();
		/// <summary>
		/// Ordered collection of stations based on proximity to a given location
		/// </summary>
		public ObservableCollection<NetMember> NearestStations
		{
			get
			{
				return _nearestStations;
			}
		}

		private NetLocation _queryLocation = new NetLocation();
		/// <summary>
		/// Location used to identify and sort nearby stations
		/// </summary>
		public NetLocation QueryLocation
		{
			get
			{
				return _queryLocation;
			}
			private set
			{
				_queryLocation.PropertyChanged -= new PropertyChangedEventHandler( _queryLocation_PropertyChanged );
				_queryLocation = value;
				_queryLocation.PropertyChanged += new PropertyChangedEventHandler( _queryLocation_PropertyChanged );
				OnPropertyChanged( "QueryLocation" );
				OnPropertyChanged( "QueryLocationText" );
			}
		}

		void _queryLocation_PropertyChanged( object sender, PropertyChangedEventArgs e )
		{
			if ( e.PropertyName == "Location" )
			{
				OnPropertyChanged( "QueryLocationText" );
				_nearestStations.Clear();
				foreach ( NetMember item in _members )
					AdjustNearestStationSort( item );
			}
		}

		/// <summary>
		/// Location text entered via UI to identify and sort nearby stations
		/// </summary>
		public string QueryLocationText
		{
			get
			{
				return _queryLocation.Location;
			}
			set
			{
				QueryLocation = new NetLocation( value );
			}
		}

		private NetMember _selectedMember;
		/// <summary>
		/// Currently selected station in roster (null if none selected)
		/// </summary>
		public NetMember SelectedMember
		{
			get
			{
				return _selectedMember;
			}
			set
			{
				_selectedMember = value;
				_mapProxy.ClearTempMarker();
				if ( _selectedMember != null )
				{
					_selectedMember.StartNewReport();
					_mapProxy.PanTo( _selectedMember.Callsign );
				}
				SetSelectedReports();
				OnPropertyChanged( "SelectedMember" );
				OnCanExecuteChanged();
			}
		}

		private ObservableCollection<NetActivity> _activities = new ObservableCollection<NetActivity>();
		/// <summary>
		/// All activities that have occurred during the active net
		/// </summary>
		public ObservableCollection<NetActivity> Activities
		{
			get
			{
				return _activities;
			}
		}

		private ObservableCollection<NetActivity> _selectedReports = new ObservableCollection<NetActivity>();
		/// <summary>
		/// All activities reported by the selected station
		/// </summary>
		public ObservableCollection<NetActivity> SelectedReports
		{
			get
			{
				return _selectedReports;
			}
		}

		private ObservableCollection<NetLoggerStatus> _loggers = new ObservableCollection<NetLoggerStatus>();
		/// <summary>
		/// All loggers that have checked into the active net (including inactive loggers)
		/// </summary>
		public ObservableCollection<NetLoggerStatus> Loggers
		{
			get
			{
				return _loggers;
			}
		}

		/// <summary>
		/// List of possible message recipients, equal to <see cref="Loggers"/> minus the current logger, plus an entry representing all loggers
		/// </summary>
		public IEnumerable<string> MessageRecipients
		{
			get
			{
				string[]
					allLoggers = new string[] { Properties.Resources.AllLoggers };
				IEnumerable<string>
					otherLoggers;

				otherLoggers = ( from ol in _loggers
								 where ol.LoggerId != LoggerId
								 orderby ol.LoggerId
								 select ol.LoggerId );

				return allLoggers.Union( otherLoggers ).ToArray();
			}
		}

		private string _messageRecipient;
		/// <summary>
		/// Selected recipient to which a new message will be sent
		/// </summary>
		public string MessageRecipient
		{
			get
			{
				return _messageRecipient;
			}
			set
			{
				_messageRecipient = value;
				OnPropertyChanged( "MessageRecipient" );
				OnCanExecuteChanged();
			}
		}

		private string _messageBody;
		/// <summary>
		/// Message body to include in new message
		/// </summary>
		public string MessageBody
		{
			get
			{
				return _messageBody ?? string.Empty;
			}
			set
			{
				_messageBody = value;
				OnPropertyChanged( "MessageBody" );
				OnCanExecuteChanged();
			}
		}

		private ObservableCollection<NetMessage> _messages = new ObservableCollection<NetMessage>();
		/// <summary>
		/// All sent and received messages
		/// </summary>
		public ObservableCollection<NetMessage> Messages
		{
			get
			{
				return _messages;
			}
		}

		private int? _activeLoggerCount;
		/// <summary>
		/// Gets the count of loggers that are identified as actively monitoring the net
		/// </summary>
		public int ActiveLoggerCount
		{
			get
			{
				if ( !_activeLoggerCount.HasValue )
				{
					_activeLoggerCount = ( from l in _loggers
										   where l.IsCurrent
										   select l ).Count();
				}

				return _activeLoggerCount.Value;
			}
		}

		private string[] _frequencies;
		/// <summary>
		/// Possible frequencies configured for net operations
		/// </summary>
		public string[] Frequencies
		{
			get
			{
				return _frequencies;
			}
		}

		private string _selectedFrequency;
		/// <summary>
		/// Current frequency for the active net
		/// </summary>
		public string SelectedFrequency
		{
			get
			{
				return _selectedFrequency;
			}
			set
			{
				NetPost
					post = new NetPost( NetsPage );

				post.Data[ "net_id" ] = ActivationId.ToString();
				post.Data[ "frequency" ] = value;
				QueueNetPost( post );

				AddActivity( new NetActivity( DateTimeOffset.UtcNow, NetActivityTypes.FrequencyChanged, string.Format(
					Properties.Resources.FrequencyChangedDetails, _selectedFrequency, value ) ) );
				_selectedFrequency = value;
			}
		}

		private Dictionary<string, NetFormerMember> _formerMembers = new Dictionary<string, NetFormerMember>();

		private string _rosterHeader;
		/// <summary>
		/// Gets the header text for the station roster list
		/// </summary>
		public string RosterHeader
		{
			get
			{
				if ( _rosterHeader == null )
					_rosterHeader = string.Format( Properties.Resources.RosterHeaderFormat, _members.Count( m => m.IsActive ), _members.Count );

				return _rosterHeader;
			}
		}

		/// <summary>
		/// Online record time from last sync
		/// </summary>
		public DateTimeOffset RecordTime
		{
			get
			{
				return _recordTime;
			}
		}

		/// <summary>
		/// Logger callsign
		/// </summary>
		public string LoggerId
		{
			get;
			private set;
		}

		/// <summary>
		/// Name for current net activation
		/// </summary>
		public string Name
		{
			get;
			private set;
		}

		/// <summary>
		/// Internal unique ID for current net activation
		/// </summary>
		public int ActivationId
		{
			get;
			private set;
		}

		/// <summary>
		/// Text entered via UI for adding a new station to the roster or finding a checked-in station
		/// </summary>
		public string CallsignSearch
		{
			get
			{
				return _callsignSearch;
			}
			set
			{
				_callsignSearch = value;
				if ( !string.IsNullOrWhiteSpace( _callsignSearch ) )
					SelectMember( _callsignSearch );
				OnPropertyChanged( "IsCallsignExactMatch" );
			}
		}

		/// <summary>
		/// True if text entered via UI exactly matches a checked-in station or formerly-checked-in station
		/// </summary>
		public bool IsCallsignExactMatch
		{
			get
			{
				if ( !string.IsNullOrWhiteSpace( _callsignSearch ) )
				{
					return GetMember( _callsignSearch ) != null || _formerMembers.ContainsKey( _callsignSearch ) ||
						_licenseData.IsLicenseDefined( _callsignSearch );
				}
				else
					return false;
			}
		}

		private NetServerStatuses _uploadStatus;
		/// <summary>
		/// Current status of background upload operations
		/// </summary>
		public NetServerStatuses UploadStatus
		{
			get
			{
				return _uploadStatus;
			}
			set
			{
				_uploadStatus = value;
				OnPropertyChangedBackground( _window, "UploadStatus" );
			}
		}

		private NetServerStatuses _syncStatus;
		/// <summary>
		/// Current status of background download operations
		/// </summary>
		public NetServerStatuses SyncStatus
		{
			get
			{
				return _syncStatus;
			}
			set
			{
				_syncStatus = value;
				OnPropertyChangedBackground( _window, "SyncStatus" );
			}
		}

		private NetServerStatuses _aprsStatus;
		/// <summary>
		/// Current status of background APRS download operations
		/// </summary>
		public NetServerStatuses AprsStatus
		{
			get
			{
				return _aprsStatus;
			}
			set
			{
				_aprsStatus = value;
				OnPropertyChangedBackground( _window, "AprsStatus" );
			}
		}

		private NetServerStatuses _licenseStatus;
		/// <summary>
		/// Current status of background FCC license data download operations
		/// </summary>
		public NetServerStatuses LicenseStatus
		{
			get
			{
				return _licenseStatus;
			}
			set
			{
				_licenseStatus = value;
				OnPropertyChangedBackground( _window, "LicenseStatus" );
			}
		}

		/// <summary>
		/// Station currently acting as net control
		/// </summary>
		public NetMember NetControl
		{
			get
			{
				return ( from m in _members
						 where m.IsNetControl
						 select m ).FirstOrDefault();
			}
		}

		/// <summary>
		/// Station currently acting as alternate net control
		/// </summary>
		public NetMember AltNetControl
		{
			get
			{
				return ( from m in _members
						 where m.IsAlternateNetControl
						 select m ).FirstOrDefault();
			}
		}

		/// <summary>
		/// Station currently acting as SKYWARN liaison
		/// </summary>
		public NetMember SKYWARNLiaison
		{
			get
			{
				return ( from m in _members
						 where m.IsSKYWARNLiaison
						 select m ).FirstOrDefault();
			}
		}

		private DateTimeOffset? _activationTime;
		/// <summary>
		/// Gets a formatted string representing net activation time
		/// </summary>
		public string ActivationTimeString
		{
			get
			{
				return _activationTime.HasValue ? _activationTime.Value.ToLocalTime().ToString( Properties.Resources.TimeFormat ) : string.Empty;
			}
		}

		private DateTimeOffset? _deactivationTime;
		/// <summary>
		/// Gets a formatted string representing net deactivation time
		/// </summary>
		public string DeactivationTimeString
		{
			get
			{
				return _deactivationTime.HasValue ? _deactivationTime.Value.ToLocalTime().ToString( Properties.Resources.TimeFormat ) : string.Empty;
			}
		}

		/// <summary>
		/// Gets the calculated elapsed time since net activation
		/// </summary>
		public TimeSpan ElapsedTime
		{
			get
			{
				TimeSpan
					t = IsActivated ? DateTimeOffset.UtcNow - _activationTime.Value : TimeSpan.Zero;

				return new TimeSpan( t.Days, t.Hours, t.Minutes, t.Seconds );
			}
		}

		/// <summary>
		/// True if net has not been deactivated
		/// </summary>
		public bool IsEnabled
		{
			get
			{
				return !_deactivationTime.HasValue;
			}
		}

		/// <summary>
		/// True if net has been activated
		/// </summary>
		public bool IsActivated
		{
			get
			{
				return _activationTime.HasValue;
			}
		}

		/// <summary>
		/// Reference to map proxy object
		/// </summary>
        public NetMapProxy MapProxy
        {
            get
            {
                return _mapProxy;
            }
        }

		/// <summary>
		/// Reference to main UI window
		/// </summary>
		public MainWindow Window
		{
			get
			{
				return _window;
			}
		}

		/// <summary>
		/// Reference to error logging object
		/// </summary>
		public NetErrorLog ErrorLog
		{
			get
			{
				return _errorLog;
			}
		}

		/// <summary>
		/// Callsign of logger that created the current net activation
		/// </summary>
		public string PrimaryLoggerId
		{
			get
			{
				return _netLoggerId ?? string.Empty;
			}
		}

		/// <summary>
		/// Starts a new net activation or connects to an existing one
		/// </summary>
		/// <returns>True if connection successful, false otherwise</returns>
		/// <remarks>
		/// This method is the starting point for creating a new net activation online or connecting to one
		/// that another logger started. It downloads a list of all currently active nets, then displays
		/// a dialog to select one of the existing nets (if any) or create a new one. Once connected, the
		/// program continues to use that net until termination.
		/// </remarks>
		public bool Connect()
		{
			XDocument
				doc = SendWebGetRequest( NetsPage );
			IEnumerable<NetFormerMember>
				former;
			IEnumerable<NetExistingActivation>
				nets;
			bool
				success = false;
			XElement
				node;

			_mapProxy = new NetMapProxy( _window.MapView );
			_mapProxy.MapClick += new EventHandler<NetMapClickEventArgs>( _mapProxy_MapClick );

			if ( doc != null )
			{
				node = doc.Descendants( "revision" ).FirstOrDefault();
				if ( node == null || ( int ) node != WebRevision )
				{
					_errorLog.Write( "Software revision {0} does not match web revision {1}, program terminating",
						WebRevision, node != null ? ( string ) node : "undefined" );
					MessageBox.Show( Properties.Resources.ErrorRevision, Properties.Resources.ErrorCaption, MessageBoxButton.OK, MessageBoxImage.Error );
					return false;
				}

				///////////////////////////////////////////////////////////////////////
				// begin the background process to download and update FCC license data
				///////////////////////////////////////////////////////////////////////
				ThreadPool.QueueUserWorkItem( new WaitCallback( BackgroundLicenseUpdate ) );

				former = ( from m in doc.Descendants( "former_members" )
						   select new NetFormerMember( m ) );
				foreach ( NetFormerMember item in former )
				{
					_formerMembers[ item.Callsign ] = item;
				}

				nets = ( from n in doc.Descendants( "nets" )
						 select new NetExistingActivation( n ) ).ToArray();

				SelectNet d = new SelectNet( nets );
				d.Owner = _window;
				if ( d.ShowDialog().GetValueOrDefault() )
				{
					Dictionary<string, string>
						data = new Dictionary<string, string>();

					if ( d.IsNewNet )
					{
						LoggerId = _netLoggerId = d.LoggerId;
						OnPropertyChanged( "PrimaryLoggerId" );
						Name = d.CompleteNetName;
						if ( !d.IsDelayedActivation.GetValueOrDefault() )
						{
							_activationTime = DateTimeOffset.UtcNow;
							data[ "activation_time" ] = NetPost.ToMySQLDate( _activationTime.Value );
							OnPropertyChanged( "ActivationTimeString" );
							OnCanExecuteChanged();
						}

						data[ "name" ] = Name;
						data[ "logger_id" ] = LoggerId;
						data[ "update_interval" ] = UpdateInterval.ToString();
						data[ "frequency" ] = SelectedFrequency;

						doc = SendWebPostRequest( NetsPage, data );
						if ( doc != null )
						{
							node = doc.Descendants( "loggers" ).FirstOrDefault();
							if ( node != null )
							{
								ActivationId = ( int ) node.Element( "net_id" );
								_recordTime = NetPost.FromMySQLDate( node.Element( "record_time" ) );
								_loggers.Add( new NetLoggerStatus( node ) );
								_activeLoggerCount = null;
								OnPropertyChanged( "ActiveLoggerCount" );
								success = true;
							}
							else
							{
								node = doc.Descendants( "error" ).FirstOrDefault();
								_errorLog.Write( "Unable to create new net ({0})", node != null ? ( string ) node : string.Empty );
							}
						}
					}
					else
					{
						LoggerId = d.LoggerId;
						Name = d.SelectedNet.Name;
						ActivationId = d.SelectedNet.ActivationId;

						data[ "net_id" ] = ActivationId.ToString();
						data[ "logger_id" ] = LoggerId;
						data[ "update_interval" ] = UpdateInterval.ToString();

						doc = SendWebPostRequest( NetsPage, data );
						if ( doc != null )
						{
							SyncUpdate( doc );
							success = true;
						}
					}

					OnPropertyChanged( "Name" );
				}
			}

			if ( success )
			{
				_sync.Start();
				_processPosts.Start();
				_queryAprs.Start();
				_uiTimer = new DispatcherTimer();
				_uiTimer.Interval = new TimeSpan( 0, 0, 1 );
				_uiTimer.Tick += new EventHandler( _uiTimer_Tick );
				if ( IsActivated )
					_uiTimer.Start();
			}

			return success;
		}

		/// <summary>
		/// Disconnects from the current net and terminates background threads
		/// </summary>
		/// <param name="doFeedback">Method called to provide UI feedback when termination takes more than a second</param>
		public void Disconnect( ExitingFeedbackDelegate doFeedback )
		{
			if ( _sync.ThreadState != ThreadState.Unstarted )
			{
				_terminate.Set();
				if ( !_sync.Join( 1000 ) )
				{
					doFeedback();
					_sync.Join();
					_processPosts.Join();
				}
				else if ( !_processPosts.Join( 1000 ) )
				{
					doFeedback();
					_processPosts.Join();
				}
				_queryAprs.Join();
			}
		}

		/// <summary>
		/// Gets a station from the roster list
		/// </summary>
		/// <param name="callsign">Callsign of station to return</param>
		/// <returns>Member object of requested station, or null if callsign is not found in roster</returns>
		public NetMember GetMember( string callsign )
		{
			return ( from m in _members
					 where m.Callsign.Equals( callsign )
					 select m ).FirstOrDefault();
		}

		/// <summary>
		/// Adds a new station to the roster, displays the station on the map, and posts the station details online
		/// </summary>
		/// <param name="member">Station to add</param>
		public void AddMember( NetMember member )
		{
			if ( !member.IsPosted )
				_scrollToMember = member;
			_members.Add( member );
			AdjustNearestStationSort( member );
			QueueNetPost( member );
			_mapProxy.UpdateMarker( member );

			_rosterHeader = null;
			OnPropertyChanged( "RosterHeader" );
		}

		/// <summary>
		/// Gets the roster member to which the list should be scrolled
		/// </summary>
		/// <param name="member">Roster member to scroll the list to, or null if no scroll is needed</param>
		/// <returns>True if a roster member is returned, false otherwise</returns>
		public bool GetScrollToMember( out object member )
		{
			if ( _scrollToMember != null )
			{
				member = _scrollToMember;
				_scrollToMember = null;
				return true;
			}
			else
			{
				member = null;
				return false;
			}
		}

		/// <summary>
		/// Selects the first station in the roster whose callsign partially matches the given string
		/// </summary>
		/// <param name="callsignSubstring">Complete or partial callsign to search for and select</param>
		/// <remarks>
		/// This method is the workhorse of the UI. As loggers type callsigns into the UI, this method searches
		/// the roster for the first station that completely or partially matches the entered text. If one is found,
		/// that station is selected in the roster and the station details are shown in the other fields on the
		/// main window. If a match is not found, the method assumes that a new station is being entered and begins
		/// a new roster member object that may eventually be added to the roster.
		/// </remarks>
		public void SelectMember( string callsignSubstring )
		{
			NetMember
				member;

			member = ( from m in _members
					   where m.Callsign.Contains( callsignSubstring )
					   select m ).FirstOrDefault();

			if ( member != null )
			{
				SelectedMember = member;
			}
			else
			{
				if ( SelectedMember == null || SelectedMember.IsPosted )
				{
					SelectedMember = null;
					SelectedMember = new NetMember();
				}

				SelectedMember.Callsign = callsignSubstring;
			}
		}

		/// <summary>
		/// Automatically fills in station details for a new roster member from a station check-in from a previous net
		/// </summary>
		/// <remarks>
		/// This method uses station details from a previous net check-in (mainly name and location) to fill a new
		/// check-in to the current net by matching the entered callsign to a former check-in. The callsign must
		/// match exactly before the previous details are used.
		/// </remarks>
		public void AutoFillMember()
		{
			if ( SelectedMember != null && !SelectedMember.IsPosted && string.IsNullOrWhiteSpace( SelectedMember.Name ) &&
				!SelectedMember.Location.IsDefined )
			{
				NetFormerMember
					formerMember;
				LicenseDetails
					licenseDetails;

				if ( GetFormerMember( SelectedMember.Callsign, out formerMember ) )
					SelectedMember.Initialize( formerMember );
				else if ( GetFCCLicenseDetails( SelectedMember.Callsign, out licenseDetails ) )
					SelectedMember.Initialize( licenseDetails );
			}
		}

		/// <summary>
		/// Gets the details for a station from a previous net check-in
		/// </summary>
		/// <param name="callsign">Callsign to search for</param>
		/// <param name="formerMember">Station details from previous check-in, or null if no callsign match</param>
		/// <returns>True if callsign matches a former check-in, false otherwise</returns>
		public bool GetFormerMember( string callsign, out NetFormerMember formerMember )
		{
			return _formerMembers.TryGetValue( callsign, out formerMember );
		}

		/// <summary>
		/// Gets the license data for a station from the FCC data
		/// </summary>
		/// <param name="callsign">Callsign to search for</param>
		/// <param name="licenseDetails">License data for the given callsign, or null if no callsign match</param>
		/// <returns>True if license data found for the given callsign, false otherwise</returns>
		public bool GetFCCLicenseDetails( string callsign, out LicenseDetails licenseDetails )
		{
			licenseDetails = _licenseData.GetDetails( callsign );
			return _licenseData != null;
		}

		/// <summary>
		/// Re-groups roster members in the UI if a member property changes that affects the grouping
		/// </summary>
		/// <param name="member">Roster member that has updated properties</param>
		public void AdjustMemberGrouping( NetMember member )
		{
			bool
				isSelected = ( SelectedMember == member );

			_members.Remove( member );
			_members.Add( member );
			if ( isSelected )
				SelectedMember = member;
		}

		/// <summary>
		/// Re-sorts roster members in the UI when a member's location changes
		/// </summary>
		/// <param name="member">Roster member with changed location</param>
		public void AdjustNearestStationSort( NetMember member )
		{
			_nearestStations.Remove( member );
			member.CalculateDistanceToLocation( _queryLocation );
			_nearestStations.Add( member );
		}

		/// <summary>
		/// Sets the default message recipient based on the most recent message shown in the list
		/// </summary>
		/// <remarks>
		/// This function assumes that a new message may be a reply to the most recent message received or a
		/// follow-up to the last message sent, so it uses the message at the top of the list as the basis
		/// for setting a default recipient to ensure that the new message goes back to the last person
		/// (if a private message) or all loggers (if not private).
		/// </remarks>
		public void SetDefaultMessageRecipient()
		{
			if ( _messages.Count > 0 )
				MessageRecipient = _messages[ 0 ].DefaultReplyTo;
			else
				MessageRecipient = Properties.Resources.AllLoggers;
		}

		/// <summary>
		/// Returns list of valid APRS IDs that partially match the given callsign
		/// </summary>
		/// <param name="callsign">Callsign to search for</param>
		/// <returns>Enumeration of all valid APRS IDs that begin with <paramref name="callsign"/></returns>
		public IEnumerable<string> GetAprsMatches( string callsign )
		{
			lock ( _aprsData )
			{
				return ( from ad in _aprsData.Keys
						 where ad.StartsWith( callsign )
						 orderby ad
						 select ad ).ToArray();
			}
		}

		/// <summary>
		/// Adds a new activity to the current net activation and posts the activity online
		/// </summary>
		/// <param name="activity">Net activity to add</param>
		/// <remarks>
		/// This method is used to add activities from the current logger as well as those downloaded
		/// from the web that were posted by other loggers. It checks to make sure the activity is not
		/// already in the list before adding it. If the activity has not been posted online yet, it
		/// posts it.
		/// </remarks>
		public void AddActivity( NetActivity activity )
		{
			if ( !_activities.Contains( activity ) )
			{
				_activities.Add( activity );
				QueueNetPost( activity );
			}
		}

		/// <summary>
		/// Queues an item for online posting
		/// </summary>
		/// <param name="updatedItem">Object containing data to post online</param>
		/// <remarks>
		/// The object's interface is used to query for the data to post online. If data is returned,
		/// it is queued for posting, otherwise nothing happens.
		/// </remarks>
		public void QueueNetPost( INetPost updatedItem )
		{
			NetPost
				post = updatedItem.GetNetPost();

			if ( post != null )
				QueueNetPost( post );
		}

		/// <summary>
		/// Internal method to queue an item to a background thread for online posting
		/// </summary>
		/// <param name="post">Object containing data to post online</param>
		private void QueueNetPost( NetPost post )
		{
			lock ( _netPosts )
			{
				_netPosts.AddLast( post );
			}

			_newPost.Set();
		}

		/// <summary>
		/// Sends an HTTP POST request containing the given data to the specified web page
		/// </summary>
		/// <param name="pageName">Page name to send the request to</param>
		/// <param name="data">Name/value pairs to post to web page</param>
		/// <param name="timeout">Time (ms) to wait for web response before returning an error</param>
		/// <returns>XML document containing response from web page post, null if an error occurs</returns>
		public static XDocument SendWebPostRequest( string pageName, Dictionary<string, string> data, int timeout = 0 )
		{
			HttpWebRequest
				request = ( HttpWebRequest ) WebRequest.Create( WebUrl + pageName );
			StringBuilder
				postString = new StringBuilder();
			byte[]
				postData;
			UTF8Encoding
				encoding = new UTF8Encoding();
			XDocument
				responseDocument;

			if ( data.Count == 0 )
				throw new ArgumentException();

			foreach ( var item in data )
			{
				postString.AppendFormat( "{0}={1}&", item.Key, Uri.EscapeDataString( item.Value ) );
			}

			request.Method = "POST";
			request.ContentType = WebRequestContentType;

			--postString.Length;
			postData = encoding.GetBytes( postString.ToString() );
			request.ContentLength = postData.Length;
			if ( timeout != 0 )
				request.Timeout = timeout;

			try
			{
				using ( Stream outStream = request.GetRequestStream() )
				{
					outStream.Write( postData, 0, postData.Length );
				}

				using ( HttpWebResponse response = ( HttpWebResponse ) request.GetResponse() )
				{
					using ( Stream inStream = response.GetResponseStream() )
					{
						responseDocument = XDocument.Load( inStream );
					}
				}
			}
			catch ( Exception e )
			{
				responseDocument = null;
				_this._errorLog.Write( "Error in SendWebPostRequest for {0}{1}{2}{1}{3}", request.RequestUri.AbsoluteUri,
					Environment.NewLine, postString, e );
			}

			return responseDocument;
		}

		/// <summary>
		/// Sends an HTTP GET request with optional data appended to the URL to the specified web page
		/// </summary>
		/// <param name="pageName">Page name to send the request to</param>
		/// <param name="data">Name/value pairs to include in URL, null if no values need to be appended</param>
		/// <param name="timeout">Time (ms) to wait for web response before returning an error</param>
		/// <returns>XML document containing response from web page post, null if an error occurs</returns>
		public static XDocument SendWebGetRequest( string pageName, Dictionary<string, string> data = null, int timeout = 0 )
		{
			HttpWebRequest
				request;
			StringBuilder
				urlString = new StringBuilder();
			UTF8Encoding
				encoding = new UTF8Encoding();
			XDocument
				responseDocument;

			if ( data != null && data.Count == 0 )
				throw new ArgumentException();

			urlString.AppendFormat( "{0}{1}?", pageName.StartsWith( "http" ) ? string.Empty : WebUrl, pageName );

			if ( data != null )
			{
				foreach ( var item in data )
				{
					urlString.AppendFormat( "{0}={1}&", item.Key, Uri.EscapeDataString( item.Value ) );
				}
			}

			--urlString.Length;
			request = ( HttpWebRequest ) WebRequest.Create( urlString.ToString() );
			if ( timeout != 0 )
				request.Timeout = timeout;

			try
			{
				using ( HttpWebResponse response = ( HttpWebResponse ) request.GetResponse() )
				{
					using ( Stream inStream = response.GetResponseStream() )
					{
						responseDocument = XDocument.Load( inStream );
					}
				}
			}
			catch ( Exception e )
			{
				responseDocument = null;
				_this._errorLog.Write( "Error in SendWebGetRequest for {0}{1}{2}", request.RequestUri.AbsoluteUri,
					Environment.NewLine, e );
			}

			return responseDocument;
		}

		/// <summary>
		/// Internal background method to process online posts
		/// </summary>
		private void ProcessPosts()
		{
			WaitHandle[]
				handles = new EventWaitHandle[] { _newPost, _terminate };
			bool
				postSuccessful,
				isTerminated = false;
			XDocument
				doc;
			NetPost[]
				posts;
			int
				retryCount;

			do
			{
				UploadStatus = NetServerStatuses.ServerOk;
				switch ( WaitHandle.WaitAny( handles ) )
				{
					case 1:
						isTerminated = true;
						break;

					case 0:
						UploadStatus = NetServerStatuses.ServerUpdate;
						lock ( _netPosts )
						{
							posts = _netPosts.ToArray();
							_netPosts.Clear();
						}
						for ( int i = 0; i < posts.Length; i++ )
						{
							retryCount = 0;
							do
							{
								doc = SendWebPostRequest( posts[ i ].Page, posts[ i ].Data, 10000 );
								postSuccessful = ( doc != null );
								if ( !postSuccessful )
								{
									switch ( ++retryCount )
									{
										case 1:
											UploadStatus = NetServerStatuses.ServerWarning;
											break;
										case 5:
											UploadStatus = NetServerStatuses.ServerError;
											break;
									}
									Thread.Sleep( PostErrorWait );
								}
							} while ( !postSuccessful );
						}
						break;
				}
			} while ( !isTerminated );
		}

		/// <summary>
		/// Internal background method to periodically download online data posted by other loggers
		/// </summary>
		private void Sync()
		{
			bool
				isTerminated;
			Dictionary<string, string>
				data = new Dictionary<string, string>();
			XDocument
				doc;
			SyncUpdateDelegate
				syncUpdate = new SyncUpdateDelegate( SyncUpdate );
			int
				failCount = 0;

			data[ "net_id" ] = ActivationId.ToString();
			data[ "logger_id" ] = LoggerId;

			do
			{
				if ( failCount == 0 )
					SyncStatus = NetServerStatuses.ServerOk;
				isTerminated = _terminate.WaitOne( UpdateInterval * 1000 );
				if ( !isTerminated )
				{
					SyncStatus = NetServerStatuses.ServerUpdate;
					data[ "record_time" ] = NetPost.ToMySQLDate( _recordTime );
					doc = SendWebPostRequest( SyncPage, data, 10000 );
					if ( doc != null )
					{
						_window.Dispatcher.BeginInvoke( syncUpdate, doc );
						failCount = 0;
					}
					else
					{
						if ( ++failCount < 3 )
							SyncStatus = NetServerStatuses.ServerWarning;
						else
							SyncStatus = NetServerStatuses.ServerError;
					}
				}
			} while ( !isTerminated );
		}

		/// <summary>
		/// Internal background method to periodically query the APRS DB for updates
		/// </summary>
		private void QueryAprs()
		{
			MySqlDataReader
				reader;
			bool
				isTerminated;
			string
				command;
			NetAprs
				aprsData;
			UpdateAprsAllDelegate
				aprsUpdate = new UpdateAprsAllDelegate( UpdateAprsAll );
			int
				failCount = 0;

			command = string.Format( "SELECT source, packet_date, latitude, longitude FROM lastposition WHERE latitude >= {0} AND latitude <= {1} AND longitude >= {2} AND longitude <= {3}",
				MinLatitude, MaxLatitude, MinLongitude, MaxLongitude );

			do
			{
				AprsStatus = NetServerStatuses.ServerUpdate;
				try
				{
					reader = MySqlHelper.ExecuteReader( AprsConnectionString, command );

					lock ( _aprsData )
					{
						_aprsData.Clear();

						while ( reader.Read() )
						{
							aprsData = new NetAprs( reader );
							_aprsData[ aprsData.AprsId ] = aprsData;
						}
					}

					reader.Close();
					_window.Dispatcher.BeginInvoke( aprsUpdate );

					failCount = 0;
					AprsStatus = NetServerStatuses.ServerOk;
				}
				catch ( MySqlException e )
				{
					_errorLog.Write( "Error reading from APRS DB{0}{1}", Environment.NewLine, e );
					if ( ++failCount < 3 )
						AprsStatus = NetServerStatuses.ServerWarning;
					else
						AprsStatus = NetServerStatuses.ServerError;
				}

				isTerminated = _terminate.WaitOne( AprsQueryInterval );
			} while ( !isTerminated );
		}

		/// <summary>
		/// Internal method to update station locations based on APRS updates
		/// </summary>
		private void UpdateAprsAll()
		{
			NetAprs
				aprsData;

			lock ( _aprsData )
			{
				foreach ( NetMember item in _members )
				{
					if ( item.IsMobile && !item.Location.IsGps && item.HasAprsId && _aprsData.TryGetValue( item.AprsId, out aprsData ) )
						item.UpdateAprs( aprsData );
					if ( !_aprsMatchesUpdated )
						item.UpdateAprsMatches();
				}

				_aprsMatchesUpdated = true;
			}
		}

		/// <summary>
		/// Updates the APRS data for a given station
		/// </summary>
		/// <param name="member">Roster member to update</param>
		/// <returns>True if roster member updated, false otherwise</returns>
		public bool UpdateAprs( NetMember member )
		{
			NetAprs
				aprsData;
			bool
				updated;

			lock ( _aprsData )
			{
				if ( updated = _aprsData.TryGetValue( member.AprsId, out aprsData ) )
					member.UpdateAprs( aprsData );
			}

			return updated;
		}

		/// <summary>
		/// Internal method to update data for the current net activation with data downloaded during sync operation
		/// </summary>
		/// <param name="doc">XML document containing downloaded data</param>
		private void SyncUpdate( XDocument doc )
		{
			NetLoggerStatus
				logger;
			NetMember
				member;
			NetMessage
				message;
			string
				loggerId;
			bool
				membersChanged = false;
			XElement
				element;

			foreach ( XElement item in doc.Descendants( "nets" ) )
			{
				foreach ( XElement ele in item.Elements() )
				{
					switch ( ele.Name.LocalName )
					{
						case "logger_id":
							_netLoggerId = NetPost.DecodedString( ele );
							OnCanExecuteChanged();
							OnPropertyChanged( "PrimaryLoggerId" );
							break;
						case "activation_time":
							_activationTime = NetPost.FromMySQLDate( ele );
							OnPropertyChanged( "ActivationTimeString" );
							OnCanExecuteChanged();
							if ( _uiTimer != null && !_uiTimer.IsEnabled )
								_uiTimer.Start();
							break;
						case "deactivation_time":
							_deactivationTime = NetPost.FromMySQLDate( ele );
							OnPropertyChanged( "DeactivationTimeString" );
							OnCanExecuteChanged();
							if ( _uiTimer != null && _uiTimer.IsEnabled )
								_uiTimer.Stop();
							_window.Close();
							break;
						case "frequency":
							_selectedFrequency = NetPost.DecodedString( ele );
							OnPropertyChanged( "SelectedFrequency" );
							break;
					}
				}
			}

			foreach ( XElement item in doc.Descendants( "loggers" ) )
			{
				loggerId = NetPost.DecodedString( item.Element( "logger_id" ) );
				logger = _loggers.Where( l => l.LoggerId == loggerId ).FirstOrDefault();

				if ( logger != null )
					logger.Update( item );
				else
				{
					_loggers.Add( new NetLoggerStatus( item ) );
					OnPropertyChanged( "MessageRecipients" );
				}

				if ( loggerId == LoggerId )
					_recordTime = NetPost.FromMySQLDate( item.Element( "record_time" ) );

				_activeLoggerCount = null;
				OnPropertyChanged( "ActiveLoggerCount" );
            }

			foreach ( XElement item in doc.Descendants( "roster" ) )
			{
				member = null;
				element = item.Element( "prev_roster_id" );
				if ( element != null )
					member = GetMember( NetPost.DecodedString( element ) );
				if ( member == null )
					member = GetMember( NetPost.DecodedString( item.Element( "roster_id" ) ) );
				if ( member != null )
					member.Update( item );
				else
					AddMember( new NetMember( item ) );

				membersChanged = true;
			}

			foreach ( XElement item in doc.Descendants( "activity" ) )
			{
				AddActivity( new NetActivity( item ) );
			}

			foreach ( XElement item in doc.Descendants( "messages" ) )
			{
				message = new NetMessage( item );
				if ( !_messages.Contains( message ) )
					_messages.Insert( 0, message );
			}

			if ( membersChanged )
			{
				OnPropertyChanged( "NetControl" );
				OnPropertyChanged( "AltNetControl" );
				OnPropertyChanged( "SKYWARNLiaison" );
			}
		}

		/// <summary>
		/// Internal method to select the station reports for the currently selected station
		/// </summary>
		private void SetSelectedReports()
		{
			_selectedReports.Clear();
			if ( SelectedMember != null )
			{
				foreach ( NetActivity item in SelectedMember.Reports )
					_selectedReports.Add( item );
			}
		}

		/// <summary>
		/// Internal method to download updated FCC license data from the server and update the local file on a background thread
		/// </summary>
		/// <param name="unused">Not used</param>
		private void BackgroundLicenseUpdate( object unused )
		{
			LicenseStatus = NetServerStatuses.ServerUpdate;

			try
			{
				XDocument
					doc;
				NetPost
					netPost = new NetPost( LicenseUpdatesPage );
				IEnumerable<LicenseDetails>
					licenses;

				netPost.Data[ "license_update_time" ] = NetPost.ToMySQLDate( _licenseData.UpdateTime );
				doc = SendWebGetRequest( netPost.Page, netPost.Data );
				if ( doc != null )
				{
					licenses = ( from lu in doc.Descendants( "license_updates" )
								 select new LicenseDetails( lu ) );
					foreach ( LicenseDetails item in licenses )
						_licenseData.UpdateLicense( item );
					_licenseData.UpdateLicenseDataFile();

					LicenseStatus = NetServerStatuses.ServerOk;
				}
				else
				{
					_errorLog.Write( "Failed to download updated FCC license data" );
					LicenseStatus = NetServerStatuses.ServerError;
				}
			}
			catch ( Exception e )
			{
				_errorLog.Write( "Error updating FCC license data{0}{1}", Environment.NewLine, e );
				LicenseStatus = NetServerStatuses.ServerError;
			}
		}

		/// <summary>
		/// Internal method to update elapsed timer
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		void _uiTimer_Tick( object sender, EventArgs e )
		{
			OnPropertyChanged( "ElapsedTime" );
		}

		/// <summary>
		/// Internal method to process map clicks
		/// </summary>
		/// <param name="sender">Object activating the map click event</param>
		/// <param name="e">Event arguments</param>
		void _mapProxy_MapClick( object sender, NetMapClickEventArgs e )
		{
			if ( e.IsMarker )
			{
				if ( e.Marker is NetMember )
					SelectedMember = ( NetMember ) e.Marker;
				else
				{
					SelectedMember = null;
					var selectedReports = ( from a in _activities
											where a is NetReport && e.Marker.Equals( a )
											select a );
					foreach ( NetActivity item in selectedReports )
					{
						_selectedReports.Add( item );
					}
				}
			}
			else if ( SelectedMember != null && SelectedMember.Location.CanUpdateLocation )
			{
				string
					message;

				message = string.Format( Properties.Resources.MapClickWarning, SelectedMember );

				if ( MessageBox.Show( message, Properties.Resources.MapClickCaption, MessageBoxButton.YesNo, MessageBoxImage.Warning,
					MessageBoxResult.No ) == MessageBoxResult.Yes )
				{
					SelectedMember.Location.SetCoordinates( e );
				}
			}
		}

		private NetActivityTypes ActivityTypeFromCommand( object parameter )
		{
			return ( NetActivityTypes ) Convert.ToInt32( parameter );
		}

		public bool CanExecute( object parameter )
		{
			switch ( ActivityTypeFromCommand( parameter ) )
			{
				case NetActivityTypes.Activation:
					return !_activationTime.HasValue && LoggerId == _netLoggerId;
				case NetActivityTypes.Deactivation:
					return _activationTime.HasValue && !_deactivationTime.HasValue && LoggerId == _netLoggerId;
				case NetActivityTypes.CheckIn:
					return _selectedMember != null;
				case NetActivityTypes.NetControlChanged:
					return _selectedMember != null;
				case NetActivityTypes.AlternateNetControlChanged:
					return _selectedMember != null;
				case NetActivityTypes.SKYWARNLiaisonChanged:
					return _selectedMember != null;
				case NetActivityTypes.MobileBaseChanged:
					return _selectedMember != null;
				case NetActivityTypes.HasTrafficChanged:
					return _selectedMember != null;
				case NetActivityTypes.SwapControlAlt:
					return _members.Any( m => m.IsAlternateNetControl );
				case NetActivityTypes.ClearNewMembers:
					return _members.Any( m => m.IsNewAddition );
				case NetActivityTypes.SelectNextWithTraffic:
					return _members.Any( m => m.HasTraffic );
				case NetActivityTypes.SendMessage:
					return !string.IsNullOrEmpty( _messageRecipient ) && !string.IsNullOrWhiteSpace( _messageBody );
				default:
					return false;
			}
		}

		public event EventHandler CanExecuteChanged;

		private void OnCanExecuteChanged()
		{
			EventHandler
				h = CanExecuteChanged;

			if ( h != null )
				h( this, new EventArgs() );
		}

		/// <summary>
		/// Executes commands based on UI selection (tool bar or function key)
		/// </summary>
		/// <param name="parameter">Identifies command to execute</param>
		public void Execute( object parameter )
		{
			NetPost
				post;
			NetMember[]
				members;
			NetMember
				member;
			NetMessage
				message;

			switch ( ActivityTypeFromCommand( parameter ) )
			{
				case NetActivityTypes.Activation:
					_activationTime = DateTimeOffset.UtcNow;
					OnPropertyChanged( "ActivationTimeString" );
					post = new NetPost( NetsPage );
					post.Data[ "net_id" ] = ActivationId.ToString();
					post.Data[ "logger_id" ] = LoggerId;
					post.Data[ "activation_time" ] = NetPost.ToMySQLDate( _activationTime.Value );
					QueueNetPost( post );
					AddActivity( new NetActivity( _activationTime.Value, NetActivityTypes.Activation, string.Format(
						Properties.Resources.ActivationDetails, _activationTime.Value ) ) );
					_uiTimer.Start();
					OnCanExecuteChanged();
					break;

				case NetActivityTypes.Deactivation:
					ConfirmDeactivation d = new ConfirmDeactivation( _members.Where( m => m.IsActive && m.IsMobile ) );
					d.Owner = _window;
					if ( d.ShowDialog().GetValueOrDefault() )
					{
						_uiTimer.Stop();
						_deactivationTime = DateTimeOffset.UtcNow;
						OnPropertyChanged( "DeactivationTimeString" );
						post = new NetPost( NetsPage );
						post.Data[ "net_id" ] = ActivationId.ToString();
						post.Data[ "logger_id" ] = LoggerId;
						post.Data[ "deactivation_time" ] = NetPost.ToMySQLDate( _deactivationTime.Value );
						QueueNetPost( post );
						post = new NetActivity( _deactivationTime.Value, NetActivityTypes.Deactivation, string.Format(
							Properties.Resources.DeactivationDetails, _deactivationTime.Value ) ).GetNetPost();
						QueueNetPost( post );
						_window.Close();
					}
					break;

				case NetActivityTypes.CheckIn:
					_selectedMember.IsActive = !_selectedMember.IsActive;
					_rosterHeader = null;
					OnPropertyChanged( "RosterHeader" );
					break;
				case NetActivityTypes.NetControlChanged:
					_selectedMember.IsNetControl = true;
					OnPropertyChanged( "NetControl" );
					OnPropertyChanged( "AltNetControl" );
					break;
				case NetActivityTypes.AlternateNetControlChanged:
					_selectedMember.IsAlternateNetControl = !_selectedMember.IsAlternateNetControl;
					OnPropertyChanged( "AltNetControl" );
					break;
				case NetActivityTypes.SKYWARNLiaisonChanged:
					_selectedMember.IsSKYWARNLiaison = !_selectedMember.IsSKYWARNLiaison;
					OnPropertyChanged( "SKYWARNLiaison" );
					break;
				case NetActivityTypes.MobileBaseChanged:
					_selectedMember.IsMobile = !_selectedMember.IsMobile;
					if ( _selectedMember.IsMobile && _selectedMember.HasAprsId )
						UpdateAprs( _selectedMember );
					break;
				case NetActivityTypes.HasTrafficChanged:
					_selectedMember.HasTraffic = !_selectedMember.HasTraffic;
					break;
				case NetActivityTypes.SwapControlAlt:
					_members.Where( m => m.IsAlternateNetControl ).First().IsNetControl = true;
					OnPropertyChanged( "NetControl" );
					OnPropertyChanged( "AltNetControl" );
					break;
				case NetActivityTypes.ClearNewMembers:
					members = _members.Where( m => m.IsNewAddition ).ToArray();
					foreach ( NetMember item in members )
						item.IsNewAddition = false;
					_window.OnFind( null, null );
					break;
				case NetActivityTypes.SelectNextWithTraffic:
					member = _members.First( m => m.HasTraffic );
					member.HasTraffic = false;
					if ( SelectedMember != member )
						SelectedMember = member;
					_window.OnNew( null, null );
					break;
				case NetActivityTypes.SendMessage:
					message = new NetMessage( ActivationId, LoggerId, _messageRecipient, _messageBody );
					QueueNetPost( message );
					MessageBody = null;
					_messages.Insert( 0, message );
					break;
			}
		}
	}
}
